把 Day1–Day4 的內容整合,完成一個 無框架的一頁式自我介紹網站,包含:
完成後,你就擁有一個「無框架也能上線」的作品。明天起把同一份 IA 搬進 Angular。
day5-portfolio/
├─ index.html
├─ styles/
│  └─ style.css       # 可先用 CSS;之後你要換 SCSS 也行
└─ scripts/
   └─ main.js         # 以 TS 思維撰寫(若要 TS 編譯可命名 main.ts)
提醒:如果你偏好 TypeScript,請以 main.ts 撰寫後用 tsc 編譯成 main.js,並在 HTML 載入 main.js 即可(寫法與 Day 3/Day 4 一致)。
語義化骨架 + 錨點導覽 + About / Skills / Projects / Contact 區塊一次就位。
<!DOCTYPE html>
<html lang="zh-Hant">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <title>Chiayu · 前端工程師 · 履歷網站</title>
  <meta name="description" content="Chiayu 的前端個人網站與作品集,含技能、專案與聯絡方式。" />
  <link rel="stylesheet" href="styles/style.css" />
</head>
<body>
  <header class="site-header">
    <div class="container">
      <a class="brand" href="#home" aria-label="回到頁面頂部">Chiayu</a>
      <nav class="site-nav" aria-label="主選單">
        <ul>
          <li><a href="#about">關於我</a></li>
          <li><a href="#skills">技能</a></li>
          <li><a href="#projects">作品</a></li>
          <li><a href="#contact">聯絡</a></li>
        </ul>
      </nav>
      <button id="theme-toggle" type="button" aria-pressed="false">切換主題</button>
    </div>
  </header>
  <main id="home">
    <!-- Hero -->
    <section class="hero container" aria-labelledby="hero-title">
      <div class="hero-text">
        <h1 id="hero-title">哈囉,我是 Chiayu</h1>
        <p>前端工程師|專長 Angular & TypeScript|喜歡把想法做成能上線的產品。</p>
        <div class="hero-cta">
          <a class="btn" href="#projects">看作品</a>
          <a class="btn btn-outline" href="#contact">聯絡我</a>
        </div>
      </div>
      <div class="hero-photo">
        <img id="avatar"
             src="assets/me-formal.jpg"
             data-alt-src="assets/me-casual.jpg"
             alt="Chiayu 的正式照片" width="240" height="240" />
        <button id="photo-toggle" class="btn small" type="button">切換照片</button>
      </div>
    </section>
    <!-- About -->
    <section id="about" class="container section" aria-labelledby="about-title">
      <h2 id="about-title">關於我</h2>
      <p>
        我是一名前端工程師,喜歡理解使用者需求並把它落地成產品。近期專注於
        Angular、TypeScript、前端架構與效能最佳化。
      </p>
      <blockquote class="quote">「持續學習,讓自己比昨天更強。」</blockquote>
      <p id="more-info" hidden>
        曾參與金融科技與電商專案,也投入設計系統與可存取性。閒暇時間喜歡健身、魔術與寫作分享。
      </p>
      <button id="toggle-more" class="btn small" type="button" aria-expanded="false" aria-controls="more-info">
        更多介紹
      </button>
    </section>
    <!-- Skills -->
    <section id="skills" class="container section" aria-labelledby="skills-title">
      <div class="section-header">
        <h2 id="skills-title">技能 Skillset</h2>
        <div id="skill-filters" role="tablist" aria-label="技能分類">
          <button role="tab" data-filter="all" aria-selected="true" class="chip">全部</button>
          <button role="tab" data-filter="frontend" class="chip">前端</button>
          <button role="tab" data-filter="backend" class="chip">後端</button>
          <button role="tab" data-filter="tools" class="chip">工具</button>
        </div>
      </div>
      <ul id="skill-list" class="skill-grid">
        <li data-category="frontend">HTML / CSS / SCSS</li>
        <li data-category="frontend">TypeScript</li>
        <li data-category="frontend">Angular / React / Vue</li>
        <li data-category="backend">Node.js / Express</li>
        <li data-category="tools">Git / GitHub / Docker</li>
        <li data-category="tools">Vite / Webpack</li>
      </ul>
    </section>
    <!-- Projects -->
    <section id="projects" class="container section" aria-labelledby="projects-title">
      <h2 id="projects-title">作品集 Projects</h2>
      <div class="project-grid">
        <article class="card">
          <h3>毛毛購物(寵物電商)</h3>
          <p class="muted">Angular + Node.js|購物車、結帳、RWD</p>
          <p>主導前端架構,完成商品列表、購物流程與訂單頁。</p>
          <a class="btn small" href="#" aria-label="查看毛毛購物專案">Live Demo</a>
        </article>
        <article class="card">
          <h3>LINE Bot 預約系統</h3>
          <p class="muted">Cloud Functions + LINE API|時段預約</p>
          <p>整合 LINE 聊天介面與雲端排程,完成會員預約流程。</p>
          <a class="btn small" href="#" aria-label="查看 LINE Bot 預約系統">Live Demo</a>
        </article>
      </div>
    </section>
    <!-- Contact -->
    <section id="contact" class="container section" aria-labelledby="contact-title">
      <h2 id="contact-title">聯絡我</h2>
      <form id="contact-form" novalidate>
        <div class="field">
          <label for="name">姓名</label>
          <input id="name" name="name" type="text" placeholder="王小明" required />
          <small class="error" data-for="name" hidden>請輸入 2–20 個字的姓名</small>
        </div>
        <div class="field">
          <label for="email">Email</label>
          <input id="email" name="email" type="email" placeholder="name@example.com" required />
          <small class="error" data-for="email" hidden>請輸入正確的 Email</small>
        </div>
        <div class="field">
          <label for="message">訊息</label>
          <textarea id="message" name="message" rows="4" placeholder="想合作的內容…" required></textarea>
          <small class="error" data-for="message" hidden>訊息至少 10 個字</small>
        </div>
        <div class="actions">
          <button class="btn" type="submit">送出</button>
          <button class="btn btn-outline" type="reset">清除</button>
        </div>
      </form>
      <aside class="contact-aside">
        <h3>其他聯絡方式</h3>
        <address>
          Email:<a href="mailto:hotdanton08@hotmail.com">hotdanton08@hotmail.com</a><br />
          GitHub:<a href="https://github.com/你的帳號">github.com/你的帳號</a><br />
          LinkedIn:<a href="https://linkedin.com/in/你的帳號">linkedin.com/in/你的帳號</a>
        </address>
      </aside>
    </section>
  </main>
  <footer class="site-footer">
    <div class="container">
      <p>© 2025 Chiayu Lee · All rights reserved.</p>
    </div>
  </footer>
  <script src="scripts/main.js" defer></script>
</body>
</html>
第一版以 CSS 即可(你也能把它翻成 SCSS,抽成變數與 mixin)。包含 Design Tokens、排版、RWD、主題切換掛鉤。
/* Design Tokens */
:root {
  --bg: #ffffff;
  --fg: #1f2937;
  --muted: #6b7280;
  --primary: #2563eb;
  --border: #e5e7eb;
  --container: 1100px;
  --radius: 12px;
  --space: 16px;
}
html[data-theme="dark"] {
  --bg: #0f172a;
  --fg: #e5e7eb;
  --muted: #9ca3af;
  --primary: #60a5fa;
  --border: #1f2937;
}
* { box-sizing: border-box; }
html, body { height: 100%; }
body {
  margin: 0;
  background: var(--bg);
  color: var(--fg);
  font-family: system-ui, -apple-system, "Noto Sans TC", Arial, sans-serif;
  line-height: 1.7;
}
/* Helpers */
.container { max-width: var(--container); margin: 0 auto; padding: 0 20px; }
.section { padding: 64px 0 48px; }
.muted { color: var(--muted); }
.btn {
  display: inline-block; border: 1px solid var(--primary); color: #fff;
  background: var(--primary); padding: 10px 16px; border-radius: 8px; text-decoration: none;
}
.btn:hover { opacity: .9; }
.btn-outline { background: transparent; color: var(--primary); }
.btn.small { padding: 6px 10px; font-size: 14px; }
/* Header */
.site-header { position: sticky; top: 0; background: var(--bg); border-bottom: 1px solid var(--border); z-index: 10; }
.site-header .container { display: flex; align-items: center; gap: 16px; padding: 12px 20px; }
.brand { font-weight: 700; color: var(--fg); text-decoration: none; }
.site-nav ul { list-style: none; margin: 0; padding: 0; display: flex; gap: 16px; }
.site-nav a { color: var(--fg); text-decoration: none; }
.site-nav a:hover { color: var(--primary); }
#theme-toggle { margin-left: auto; }
/* Hero */
.hero { display: grid; grid-template-columns: 1.2fr .8fr; align-items: center; gap: 24px; padding: 48px 0; }
.hero-cta { display: flex; gap: 12px; margin-top: 12px; }
.hero-photo { text-align: center; }
.hero-photo img { border-radius: 50%; border: 4px solid var(--border); }
/* Sections */
.section-header { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.quote { margin: 8px 0 0; padding-left: 12px; border-left: 4px solid var(--primary); color: var(--muted); }
/* Chips / Filters */
#skill-filters { display: flex; gap: 8px; flex-wrap: wrap; }
.chip {
  border: 1px solid var(--border); background: transparent; color: var(--fg);
  padding: 6px 10px; border-radius: 999px; cursor: pointer;
}
.chip[aria-selected="true"] { border-color: var(--primary); color: var(--primary); }
/* Grids */
.skill-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; padding: 0; list-style: none; }
.skill-grid li {
  border: 1px solid var(--border); padding: 10px 12px; border-radius: var(--radius);
}
.project-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 16px; }
.card { border: 1px solid var(--border); border-radius: var(--radius); padding: 16px; background: rgba(255,255,255,0.02); }
/* Contact */
.field { margin-bottom: 12px; }
label { display: block; margin-bottom: 6px; }
input, textarea {
  width: 100%; padding: 10px 12px; border: 1px solid var(--border); border-radius: 8px; background: transparent; color: var(--fg);
}
.error { color: #ef4444; display: block; margin-top: 6px; }
/* Aside */
.contact-aside { margin-top: 24px; border-top: 1px solid var(--border); padding-top: 16px; }
/* Footer */
.site-footer { margin-top: 48px; border-top: 1px solid var(--border); }
.site-footer .container { padding: 16px 20px; text-align: center; color: var(--muted); }
/* RWD */
@media (max-width: 900px) {
  .hero { grid-template-columns: 1fr; text-align: center; }
  .skill-grid { grid-template-columns: repeat(2, 1fr); }
  .project-grid { grid-template-columns: 1fr; }
}
@media (max-width: 520px) {
  .skill-grid { grid-template-columns: 1fr; }
}
以 Day 3、Day 4 的作法組合:主題切換、錨點平滑捲動、照片切換、更多介紹、技能篩選、表單驗證。
// 主題切換
const htmlEl = document.documentElement;
const themeBtn = document.querySelector('#theme-toggle');
function applyTheme(theme) {
  htmlEl.setAttribute('data-theme', theme);
  themeBtn?.setAttribute('aria-pressed', String(theme === 'dark'));
  if (themeBtn) themeBtn.textContent = theme === 'dark' ? '切換為亮色' : '切換為暗色';
  localStorage.setItem('theme', theme);
}
applyTheme(localStorage.getItem('theme') || 'light');
themeBtn?.addEventListener('click', () => {
  const next = htmlEl.getAttribute('data-theme') === 'dark' ? 'light' : 'dark';
  applyTheme(next);
});
// 導覽錨點:平滑捲動
document.querySelectorAll('a[href^="#"]').forEach(a => {
  a.addEventListener('click', (e) => {
    const id = a.getAttribute('href');
    if (!id || id === '#') return;
    const target = document.querySelector(id);
    if (target) {
      e.preventDefault();
      target.scrollIntoView({ behavior: 'smooth', block: 'start' });
    }
  });
});
// 照片切換(正式/生活)
const avatar = document.querySelector('#avatar');
document.querySelector('#photo-toggle')?.addEventListener('click', () => {
  if (!avatar) return;
  const current = avatar.getAttribute('src') || '';
  const altSrc = avatar.dataset.altSrc || '';
  if (!altSrc) return;
  avatar.setAttribute('src', altSrc);
  avatar.dataset.altSrc = current;
  const isFormal = /formal/.test(altSrc);
  avatar.alt = isFormal ? 'Chiayu 的正式照片' : 'Chiayu 的生活照片';
});
// 更多介紹展開/收起
const more = document.querySelector('#more-info');
const moreBtn = document.querySelector('#toggle-more');
moreBtn?.addEventListener('click', () => {
  if (!more) return;
  const isHidden = more.hasAttribute('hidden');
  if (isHidden) {
    more.removeAttribute('hidden');
    moreBtn.setAttribute('aria-expanded', 'true');
    moreBtn.textContent = '收起介紹';
  } else {
    more.setAttribute('hidden', '');
    moreBtn.setAttribute('aria-expanded', 'false');
    moreBtn.textContent = '更多介紹';
  }
});
// 技能分類篩選(事件委派)
const filters = document.querySelector('#skill-filters');
const skillList = document.querySelector('#skill-list');
function applySkillFilter(cat) {
  if (!skillList) return;
  skillList.querySelectorAll('li').forEach(li => {
    const c = li.dataset.category || 'frontend';
    li.style.display = (cat === 'all' || c === cat) ? '' : 'none';
  });
}
filters?.addEventListener('click', (e) => {
  const target = e.target;
  if (target.matches('button[data-filter]')) {
    filters.querySelectorAll('[aria-selected="true"]').forEach(el => el.setAttribute('aria-selected', 'false'));
    target.setAttribute('aria-selected', 'true');
    applySkillFilter(target.getAttribute('data-filter'));
  }
});
applySkillFilter('all');
// 表單驗證(姓名 2–20、email 格式、訊息 ≥ 10)
const form = document.querySelector('#contact-form');
function setError(name, show) {
  const err = form?.querySelector(`.error[data-for="${name}"]`);
  if (!err) return;
  err.hidden = !show;
}
form?.addEventListener('submit', (e) => {
  e.preventDefault();
  const name = form.querySelector('#name');
  const email = form.querySelector('#email');
  const message = form.querySelector('#message');
  let ok = true;
  if (!name.value || name.value.trim().length < 2 || name.value.trim().length > 20) {
    setError('name', true); ok = false;
  } else setError('name', false);
  const emailRe = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  if (!emailRe.test(email.value)) { setError('email', true); ok = false; } else setError('email', false);
  if (!message.value || message.value.trim().length < 10) { setError('message', true); ok = false; } else setError('message', false);
  if (ok) {
    alert('已送出!感謝你的來信。');
    form.reset();
  }
});
用 <br> 和   撐版面
錯誤: 多放 <br> 讓元素彼此拉開。
正確: 用 CSS 的 margin/padding 控距離,或用網格/彈性排版。
把 <a> 當按鈕(沒有 href)
錯誤: <a>送出</a>。
正確: 導頁用 <a href="...">;觸發動作用 <button type="button">。
直接寫 innerHTML = userInput
錯誤: 可能造成 XSS。
正確: 使用 textContent ;確實需要插入 HTML 再做消毒/模板化。
忘記處理 null、濫用非空斷言 !
錯誤:
document.querySelector('#x').addEventListener('click', fn);
正確: 先判斷元素存在再綁事件;或在載入尾端 defer。
把樣式全部用行內 style 改
錯誤: JS 裡大量 el.style.xxx = ...。
正確: 以 classList 切換狀態,把視覺交給 CSS。
fetch 打到 mock API(或用 Formspree 類服務)明天開始把這份 同版型的網站 搬進 Angular: